/**
 * Copyright (c) 2007, Gaudenz Alder
 */
package com.mxgraph.swing.view;

import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;

import java.awt.*;
import java.util.List;
import java.util.*;

/**
 * Represents the current state of a cell in a given graph view.
 */
public class mxCellStatePreview {
    /**
     * The Deltas.
     */
    protected Map<mxCellState, mxPoint> deltas = new LinkedHashMap<mxCellState, mxPoint>();

    /**
     * The Count.
     */
    protected int count = 0;

    /**
     * The Graph component.
     */
    protected mxGraphComponent graphComponent;

    /**
     * Specifies if cell states should be cloned or changed in-place.
     */
    protected boolean cloned;

    /**
     * The Opacity.
     */
    protected float opacity = 1;

    /**
     * The Cell states.
     */
    protected List<mxCellState> cellStates;

    /**
     * Constructs a new state preview. The paint handler to invoke the paint
     * method must be installed elsewhere.
     *
     * @param graphComponent the graph component
     * @param cloned         the cloned
     */
    public mxCellStatePreview(mxGraphComponent graphComponent, boolean cloned) {
        this.graphComponent = graphComponent;
        this.cloned = cloned;
    }

    /**
     * Is cloned boolean.
     *
     * @return the boolean
     */
    public boolean isCloned() {
        return cloned;
    }

    /**
     * Sets cloned.
     *
     * @param value the value
     */
    public void setCloned(boolean value) {
        cloned = value;
    }

    /**
     * Is empty boolean.
     *
     * @return the boolean
     */
    public boolean isEmpty() {
        return count == 0;
    }

    /**
     * Gets count.
     *
     * @return the count
     */
    public int getCount() {
        return count;
    }

    /**
     * Gets deltas.
     *
     * @return the deltas
     */
    public Map<mxCellState, mxPoint> getDeltas() {
        return deltas;
    }

    /**
     * Gets opacity.
     *
     * @return the opacity
     */
    public float getOpacity() {
        return opacity;
    }

    /**
     * Sets opacity.
     *
     * @param value the value
     */
    public void setOpacity(float value) {
        opacity = value;
    }

    /**
     * Move state mx point.
     *
     * @param state the state
     * @param dx    the dx
     * @param dy    the dy
     * @return the mx point
     */
    public mxPoint moveState(mxCellState state, double dx, double dy) {
        return moveState(state, dx, dy, true, true);
    }

    /**
     * Move state mx point.
     *
     * @param state        the state
     * @param dx           the dx
     * @param dy           the dy
     * @param add          the add
     * @param includeEdges the include edges
     * @return the mx point
     */
    public mxPoint moveState(mxCellState state, double dx, double dy,
                             boolean add, boolean includeEdges) {
        mxPoint delta = deltas.get(state);

        if (delta == null) {
            delta = new mxPoint(dx, dy);
            deltas.put(state, delta);
            count++;
        } else {
            if (add) {
                delta.setX(delta.getX() + dx);
                delta.setY(delta.getY() + dy);
            } else {
                delta.setX(dx);
                delta.setY(dy);
            }
        }

        if (includeEdges) {
            addEdges(state);
        }

        return delta;
    }

    /**
     * Returns a dirty rectangle to be repainted in mxGraphControl.
     *
     * @return the mx rectangle
     */
    public mxRectangle show() {
        mxGraph graph = graphComponent.getGraph();
        mxIGraphModel model = graph.getModel();

        // Stores a copy of the cell states
        List<mxCellState> previousStates = null;

        if (isCloned()) {
            previousStates = new LinkedList<mxCellState>();
            Iterator<mxCellState> it = deltas.keySet().iterator();

            while (it.hasNext()) {
                mxCellState state = it.next();
                previousStates.addAll(snapshot(state));
            }
        }

        // Translates the states in step
        Iterator<mxCellState> it = deltas.keySet().iterator();

        while (it.hasNext()) {
            mxCellState state = it.next();
            mxPoint delta = deltas.get(state);
            mxCellState parentState = graph.getView().getState(
                    model.getParent(state.getCell()));
            translateState(parentState, state, delta.getX(), delta.getY());
        }

        // Revalidates the states in step
        mxRectangle dirty = null;
        it = deltas.keySet().iterator();

        while (it.hasNext()) {
            mxCellState state = it.next();
            mxPoint delta = deltas.get(state);
            mxCellState parentState = graph.getView().getState(
                    model.getParent(state.getCell()));
            mxRectangle tmp = revalidateState(parentState, state, delta.getX(),
                    delta.getY());

            if (dirty != null) {
                dirty.add(tmp);
            } else {
                dirty = tmp;
            }
        }

        // Takes a snapshot of the states for later drawing. If the states
        // are not cloned then this does nothing and just expects a repaint
        // of the dirty rectangle.
        if (previousStates != null) {
            cellStates = new LinkedList<mxCellState>();
            it = deltas.keySet().iterator();

            while (it.hasNext()) {
                mxCellState state = it.next();
                cellStates.addAll(snapshot(state));
            }

            // Restores the previous states
            restore(previousStates);
        }

        if (dirty != null) {
            dirty.grow(2);
        }

        return dirty;
    }

    /**
     * Restore.
     *
     * @param snapshot the snapshot
     */
    public void restore(List<mxCellState> snapshot) {
        mxGraph graph = graphComponent.getGraph();
        Iterator<mxCellState> it = snapshot.iterator();

        while (it.hasNext()) {
            mxCellState state = it.next();
            mxCellState orig = graph.getView().getState(state.getCell());

            if (orig != null && orig != state) {
                restoreState(orig, state);
            }
        }
    }

    /**
     * Restore state.
     *
     * @param state the state
     * @param from  the from
     */
    public void restoreState(mxCellState state, mxCellState from) {
        state.setLabelBounds(from.getLabelBounds());
        state.setAbsolutePoints(from.getAbsolutePoints());
        state.setOrigin(from.getOrigin());
        state.setAbsoluteOffset(from.getAbsoluteOffset());
        state.setBoundingBox(from.getBoundingBox());
        state.setTerminalDistance(from.getTerminalDistance());
        state.setSegments(from.getSegments());
        state.setLength(from.getLength());
        state.setX(from.getX());
        state.setY(from.getY());
        state.setWidth(from.getWidth());
        state.setHeight(from.getHeight());
    }

    /**
     * Snapshot list.
     *
     * @param state the state
     * @return the list
     */
    public List<mxCellState> snapshot(mxCellState state) {
        List<mxCellState> result = new LinkedList<mxCellState>();

        if (state != null) {
            result.add((mxCellState) state.clone());

            mxGraph graph = graphComponent.getGraph();
            mxIGraphModel model = graph.getModel();
            Object cell = state.getCell();
            int childCount = model.getChildCount(cell);

            for (int i = 0; i < childCount; i++) {
                result.addAll(snapshot(graph.getView().getState(
                        model.getChildAt(cell, i))));
            }
        }

        return result;
    }

    /**
     * Translate state.
     *
     * @param parentState the parent state
     * @param state       the state
     * @param dx          the dx
     * @param dy          the dy
     */
    protected void translateState(mxCellState parentState, mxCellState state,
                                  double dx, double dy) {
        if (state != null) {
            mxGraph graph = graphComponent.getGraph();
            mxIGraphModel model = graph.getModel();
            Object cell = state.getCell();

            if (model.isVertex(cell)) {
                state.getView().updateCellState(state);
                mxGeometry geo = graph.getCellGeometry(cell);

                // Moves selection cells and non-relative vertices in
                // the first phase so that edge terminal points will
                // be updated in the second phase
                if ((dx != 0 || dy != 0) && geo != null
                        && (!geo.isRelative() || deltas.get(state) != null)) {
                    state.setX(state.getX() + dx);
                    state.setY(state.getY() + dy);
                }
            }

            int childCount = model.getChildCount(cell);

            for (int i = 0; i < childCount; i++) {
                translateState(state,
                        graph.getView().getState(model.getChildAt(cell, i)),
                        dx, dy);
            }
        }
    }

    /**
     * Revalidate state mx rectangle.
     *
     * @param parentState the parent state
     * @param state       the state
     * @param dx          the dx
     * @param dy          the dy
     * @return the mx rectangle
     */
    protected mxRectangle revalidateState(mxCellState parentState,
                                          mxCellState state, double dx, double dy) {
        mxRectangle dirty = null;

        if (state != null) {
            mxGraph graph = graphComponent.getGraph();
            mxIGraphModel model = graph.getModel();
            Object cell = state.getCell();

            // Updates the edge terminal points and restores the
            // (relative) positions of any (relative) children
            if (model.isEdge(cell)) {
                state.getView().updateCellState(state);
            }

            dirty = state.getView().getBoundingBox(state, false);

            // Moves selection vertices which are relative
            mxGeometry geo = graph.getCellGeometry(cell);

            if ((dx != 0 || dy != 0)
                    && geo != null
                    && geo.isRelative()
                    && model.isVertex(cell)
                    && (parentState == null
                    || model.isVertex(parentState.getCell()) || deltas
                    .get(state) != null)) {
                state.setX(state.getX() + dx);
                state.setY(state.getY() + dy);

                // TODO: Check this change
                dirty.setX(dirty.getX() + dx);
                dirty.setY(dirty.getY() + dy);

                graph.getView().updateLabelBounds(state);
            }

            int childCount = model.getChildCount(cell);

            for (int i = 0; i < childCount; i++) {
                mxRectangle tmp = revalidateState(state, graph.getView()
                        .getState(model.getChildAt(cell, i)), dx, dy);

                if (dirty != null) {
                    dirty.add(tmp);
                } else {
                    dirty = tmp;
                }
            }
        }

        return dirty;
    }

    /**
     * Add edges.
     *
     * @param state the state
     */
    public void addEdges(mxCellState state) {
        mxGraph graph = graphComponent.getGraph();
        mxIGraphModel model = graph.getModel();
        Object cell = state.getCell();
        int edgeCount = model.getEdgeCount(cell);

        for (int i = 0; i < edgeCount; i++) {
            mxCellState state2 = graph.getView().getState(
                    model.getEdgeAt(cell, i));

            if (state2 != null) {
                moveState(state2, 0, 0);
            }
        }
    }

    /**
     * Paint.
     *
     * @param g the g
     */
    public void paint(Graphics g) {
        if (cellStates != null && cellStates.size() > 0) {
            mxGraphics2DCanvas canvas = graphComponent.getCanvas();

            // Sets antialiasing
            if (graphComponent.isAntiAlias()) {
                mxUtils.setAntiAlias((Graphics2D) g, true, true);
            }

            Graphics2D previousGraphics = canvas.getGraphics();
            mxPoint previousTranslate = canvas.getTranslate();
            double previousScale = canvas.getScale();

            try {
                canvas.setScale(graphComponent.getGraph().getView().getScale());
                canvas.setTranslate(0, 0);
                canvas.setGraphics((Graphics2D) g);

                paintPreview(canvas);
            } finally {
                canvas.setScale(previousScale);
                canvas.setTranslate(previousTranslate.getX(), previousTranslate.getY());
                canvas.setGraphics(previousGraphics);
            }
        }
    }

    /**
     * Gets opacity for cell.
     *
     * @param cell the cell
     * @return the opacity for cell
     */
    protected float getOpacityForCell(Object cell) {
        return opacity;
    }

    /**
     * Draws the preview using the graphics canvas.
     *
     * @param canvas the canvas
     */
    protected void paintPreview(mxGraphics2DCanvas canvas) {
        Composite previousComposite = canvas.getGraphics().getComposite();

        // Paints the preview states
        Iterator<mxCellState> it = cellStates.iterator();

        while (it.hasNext()) {
            mxCellState state = it.next();
            canvas.getGraphics().setComposite(
                    AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                            getOpacityForCell(state.getCell())));
            paintPreviewState(canvas, state);
        }

        canvas.getGraphics().setComposite(previousComposite);
    }

    /**
     * Draws the preview using the graphics canvas.
     *
     * @param canvas the canvas
     * @param state  the state
     */
    protected void paintPreviewState(mxGraphics2DCanvas canvas,
                                     mxCellState state) {
        graphComponent.getGraph().drawState(
                canvas,
                state,
                state.getCell() != graphComponent.getCellEditor()
                        .getEditingCell());
    }
}
